using System;
using UnityEngine;

namespace Obi
{
    [AddComponentMenu("Physics/Obi/Obi Particle Attachment", 820)]
    [RequireComponent(typeof(ObiActor))]
    [ExecuteInEditMode]
    public class ObiParticleAttachment : MonoBehaviour
    {
        public enum AttachmentType
        {
            Static,
            Dynamic
        }

        [SerializeField] [HideInInspector] private ObiActor m_Actor;
        [SerializeField] [HideInInspector] private Transform m_Target;

        [SerializeField] [HideInInspector] private ObiParticleGroup m_ParticleGroup;
        [SerializeField] [HideInInspector] private AttachmentType m_AttachmentType = AttachmentType.Static;
        [SerializeField] [HideInInspector] private bool m_ConstrainOrientation = false;
        [SerializeField] [HideInInspector] private bool m_Projection = false;
        [SerializeField] [HideInInspector] private float m_Compliance = 0;

        // private variables are serialized during script reloading, to keep their value. Must mark them explicitly as non-serialized.
        [NonSerialized] private ObiPinConstraintsBatch pinBatch;
        [NonSerialized] private ObiColliderBase attachedCollider;
        [NonSerialized] private int attachedColliderHandleIndex;

        [NonSerialized] private int[] m_SolverIndices;
        [NonSerialized] private Vector3[] m_PositionOffsets = null;
        [NonSerialized] private Quaternion[] m_OrientationOffsets = null;

        /// <summary>  
        /// The actor this attachment is added to.
        /// </summary> 
        public ObiActor actor
        {
            get { return m_Actor; }
        }

        /// <summary>  
        /// The target transform that the <see cref="particleGroup"/> should be attached to.
        /// </summary> 
        public Transform target
        {
            get { return m_Target; }
            set
            {
                if (value != m_Target)
                {
                    m_Target = value;
                    Bind();
                }
            }
        }

        /// <summary>  
        /// The particle group that should be attached to the <see cref="target"/>.
        /// </summary> 
        public ObiParticleGroup particleGroup
        {
            get
            {
                return m_ParticleGroup;
            }

            set
            {
                if (value != m_ParticleGroup)
                {
                    m_ParticleGroup = value;
                    Bind();
                }
            }
        }

        /// <summary>  
        /// Whether this attachment is currently bound or not.
        /// </summary> 
        public bool isBound
        {
            get { return m_Target != null && m_SolverIndices != null && m_PositionOffsets != null; }
        }

        /// <summary>  
        /// Type of attachment, can be either static or dynamic.
        /// </summary> 
        public AttachmentType attachmentType
        {
            get { return m_AttachmentType; }
            set
            {
                if (value != m_AttachmentType)
                {
                    DisableAttachment(m_AttachmentType);
                    m_AttachmentType = value;
                    EnableAttachment(m_AttachmentType);
                }
            }
        }

        /// <summary>  
        /// Should this attachment constraint particle orientations too?
        /// </summary>
        public bool constrainOrientation
        {
            get { return m_ConstrainOrientation; }
            set
            {
                if (value != m_ConstrainOrientation)
                {
                    DisableAttachment(m_AttachmentType);
                    m_ConstrainOrientation = value;
                    EnableAttachment(m_AttachmentType);
                }
            }
        }

        /// <summary>  
        /// Should this dynamic attachment project renderable particle positions every frame to the target?
        /// </summary>
        public bool projection
        {
            get { return m_Projection; }
            set
            {
                if (value != m_Projection)
                {
                    DisableAttachment(m_AttachmentType);
                    m_Projection = value;
                    EnableAttachment(m_AttachmentType);
                }
            }
        }

        /// <summary>  
        /// Constraint compliance, in case this attachment is dynamic.
        /// </summary>
        /// High compliance values will increase the attachment's elasticity.
        public float compliance
        {
            get { return m_Compliance; }
            set
            {
                if (!Mathf.Approximately(value, m_Compliance))
                {
                    m_Compliance = value;
                    if (m_AttachmentType == AttachmentType.Dynamic && pinBatch != null)
                    {
                        for (int i = 0; i < m_SolverIndices.Length; ++i)
                            pinBatch.stiffnesses[i * 2] = m_Compliance;
                    }
                }
            }
        }

        /// <summary>  
        /// Force thershold above which the attachment should break.
        /// </summary>
        /// Only affects dynamic attachments, as static attachments do not work with forces.
        [Delayed] public float breakThreshold = float.PositiveInfinity;

        private void OnEnable()
        {
            m_Actor = GetComponent<ObiActor>();
            m_Actor.OnBlueprintLoaded += Actor_OnBlueprintLoaded;
            m_Actor.OnSimulationStart += Actor_OnSimulate;

            if (m_Actor.solver != null)
                Actor_OnBlueprintLoaded(m_Actor, m_Actor.sourceBlueprint);

            EnableAttachment(m_AttachmentType);
        }

        private void OnDisable()
        {
            DisableAttachment(m_AttachmentType);

            m_Actor.OnBlueprintLoaded -= Actor_OnBlueprintLoaded;
            m_Actor.OnSimulationStart -= Actor_OnSimulate;
        }

        private void OnValidate()
        {
            m_Actor = GetComponent<ObiActor>();

            // do not re-bind: simply disable and re-enable the attachment.
            DisableAttachment(AttachmentType.Static);
            DisableAttachment(AttachmentType.Dynamic);
            EnableAttachment(m_AttachmentType);
        }

        void Actor_OnBlueprintLoaded(ObiActor act, ObiActorBlueprint blueprint)
        {
            Bind();
        }

        void Actor_OnSimulate(ObiActor act, float stepTime, float substepTime)
        {
            // Attachments must be updated at the start of the step, before performing any simulation.
            UpdateAttachment();

            // if there's any broken constraint, flag pin constraints as dirty for remerging at the start of the next step.
            BreakDynamicAttachment(substepTime);
        }

        private void Bind()
        {
            // Disable attachment.
            DisableAttachment(m_AttachmentType);

            if (m_Target != null && m_ParticleGroup != null && m_Actor.isLoaded)
            {
                Matrix4x4 bindMatrix = m_Target.worldToLocalMatrix * m_Actor.solver.transform.localToWorldMatrix;

                m_SolverIndices = new int[m_ParticleGroup.Count];
                m_PositionOffsets = new Vector3[m_ParticleGroup.Count];
                m_OrientationOffsets = new Quaternion[m_ParticleGroup.Count];

                for (int i = 0; i < m_ParticleGroup.Count; ++i)
                {
                    int particleIndex = m_ParticleGroup.particleIndices[i];
                    if (particleIndex >= 0 && particleIndex < m_Actor.solverIndices.count)
                    {
                        m_SolverIndices[i] = m_Actor.solverIndices[particleIndex];
                        m_PositionOffsets[i] = bindMatrix.MultiplyPoint3x4(m_Actor.solver.positions[m_SolverIndices[i]]);
                    }
                    else
                    {
                        Debug.LogError("The particle group \'" + m_ParticleGroup.name + "\' references a particle that does not exist in the actor \'" + m_Actor.name + "\'.");
                        m_SolverIndices = null;
                        m_PositionOffsets = null;
                        m_OrientationOffsets = null;
                        return;
                    }
                }

                if (m_Actor.usesOrientedParticles)
                {
                    Quaternion bindOrientation = bindMatrix.rotation;

                    for (int i = 0; i < m_ParticleGroup.Count; ++i)
                    {
                        int particleIndex = m_ParticleGroup.particleIndices[i];
                        if (particleIndex >= 0 && particleIndex < m_Actor.solverIndices.count)
                            m_OrientationOffsets[i] = bindOrientation * m_Actor.solver.orientations[m_SolverIndices[i]];
                    }
                }
            }
            else
            {
                m_PositionOffsets = null;
                m_OrientationOffsets = null;
            }

            EnableAttachment(m_AttachmentType);
        }


        private void EnableAttachment(AttachmentType type)
        {
            if (enabled && m_Actor.isLoaded && isBound)
            {
                var solver = m_Actor.solver;

                switch (type)
                {
                    case AttachmentType.Dynamic:

                        var pins = m_Actor.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiPinConstraintsData;
                        attachedCollider = m_Target.GetComponent<ObiColliderBase>();

                        if (pins != null && attachedCollider != null && pinBatch == null)
                        {
                            // create a new data batch with all our pin constraints:
                            pinBatch = new ObiPinConstraintsBatch(pins);
                            for (int i = 0; i < m_SolverIndices.Length; ++i)
                            {
                                pinBatch.AddConstraint(m_SolverIndices[i],
                                                       attachedCollider,
                                                       m_PositionOffsets[i],
                                                       m_OrientationOffsets[i],
                                                       m_Compliance,
                                                       constrainOrientation ? 0 : 10000,
                                                       projection);

                                pinBatch.activeConstraintCount++;
                            }

                            // add the batch to the actor:
                            pins.AddBatch(pinBatch);

                            // store the attached collider's handle:
                            attachedColliderHandleIndex = -1;
                            if (attachedCollider.Handle != null)
                                attachedColliderHandleIndex = attachedCollider.Handle.index;

                            m_Actor.SetConstraintsDirty(Oni.ConstraintType.Pin);
                        }

                        break;

                    case AttachmentType.Static:

                        for (int i = 0; i < m_SolverIndices.Length; ++i)
                            if (m_SolverIndices[i] >= 0 && m_SolverIndices[i] < solver.invMasses.count)
                                solver.invMasses[m_SolverIndices[i]] = 0;

                        if (m_Actor.usesOrientedParticles && m_ConstrainOrientation)
                        {
                            for (int i = 0; i < m_SolverIndices.Length; ++i)
                                if (m_SolverIndices[i] >= 0 && m_SolverIndices[i] < solver.invRotationalMasses.count)
                                    solver.invRotationalMasses[m_SolverIndices[i]] = 0;
                        }

                        m_Actor.UpdateParticleProperties();

                        break;

                }
            }

        }

        private void DisableAttachment(AttachmentType type)
        {
            if (isBound)
            {
                switch (type)
                {
                    case AttachmentType.Dynamic:

                        if (pinBatch != null)
                        {
                            var pins = m_Actor.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints<ObiPinConstraintsBatch>;
                            if (pins != null)
                            {
                                pins.RemoveBatch(pinBatch);
                                if (actor.isLoaded)
                                    m_Actor.SetConstraintsDirty(Oni.ConstraintType.Pin);
                            }

                            attachedCollider = null;
                            pinBatch = null;
                            attachedColliderHandleIndex = -1;
                        }

                        break;

                    case AttachmentType.Static:

                        var solver = m_Actor.solver;
                        var blueprint = m_Actor.sourceBlueprint;

                        for (int i = 0; i < m_SolverIndices.Length; ++i)
                        {
                            int solverIndex = m_SolverIndices[i];
                            if (solverIndex >= 0 && solverIndex < solver.invMasses.count)
                                solver.invMasses[solverIndex] = blueprint.invMasses[i];
                        }

                        if (m_Actor.usesOrientedParticles)
                        {
                            for (int i = 0; i < m_SolverIndices.Length; ++i)
                            {
                                int solverIndex = m_SolverIndices[i];
                                if (solverIndex >= 0 && solverIndex < solver.invRotationalMasses.count)
                                    solver.invRotationalMasses[solverIndex] = blueprint.invRotationalMasses[i];
                            }
                        }

                        m_Actor.UpdateParticleProperties();

                        break;

                }
            }
        }

        private void UpdateAttachment()
        {

            if (enabled && m_Actor.isLoaded && isBound)
            {
                var solver = m_Actor.solver;

                switch (m_AttachmentType)
                {
                    case AttachmentType.Dynamic:

                        // in case the handle has been updated/invalidated (for instance, when disabling the target) rebuild constraints:
                        if (attachedCollider != null &&
                            attachedCollider.Handle != null &&
                            attachedCollider.Handle.index != attachedColliderHandleIndex)
                        {
                            attachedColliderHandleIndex = attachedCollider.Handle.index;
                            m_Actor.SetConstraintsDirty(Oni.ConstraintType.Pin);
                        }

                        break;

                    case AttachmentType.Static:

                        var blueprint = m_Actor.sourceBlueprint;
                        bool targetActive = m_Target.gameObject.activeInHierarchy;

                        // Build the attachment matrix:
                        Matrix4x4 attachmentMatrix = solver.transform.worldToLocalMatrix * m_Target.localToWorldMatrix;

                        // Fix all particles in the group and update their position 
                        // Note: skip assignment to startPositions if you want attached particles to be interpolated too.
                        for (int i = 0; i < m_SolverIndices.Length; ++i)
                        {
                            int solverIndex = m_SolverIndices[i];

                            if (solverIndex >= 0 && solverIndex < solver.invMasses.count)
                            {
                                if (targetActive)
                                {
                                    solver.invMasses[solverIndex] = 0;
                                    solver.velocities[solverIndex] = Vector3.zero;
                                    solver.startPositions[solverIndex] = solver.endPositions[solverIndex] = solver.positions[solverIndex] = attachmentMatrix.MultiplyPoint3x4(m_PositionOffsets[i]);
                                }
                                else
                                    solver.invMasses[solverIndex] = blueprint.invMasses[i];
                            }
                        }

                        if (m_Actor.usesOrientedParticles && m_ConstrainOrientation)
                        {
                            Quaternion attachmentRotation = attachmentMatrix.rotation;

                            for (int i = 0; i < m_SolverIndices.Length; ++i)
                            {
                                int solverIndex = m_SolverIndices[i];

                                if (solverIndex >= 0 && solverIndex < solver.invRotationalMasses.count)
                                {
                                    if (targetActive)
                                    {
                                        solver.invRotationalMasses[solverIndex] = 0;
                                        solver.angularVelocities[solverIndex] = Vector3.zero;
                                        solver.startOrientations[solverIndex] = solver.endOrientations[solverIndex] = solver.orientations[solverIndex] = attachmentRotation * m_OrientationOffsets[i];
                                    }
                                    else
                                        solver.invRotationalMasses[solverIndex] = blueprint.invRotationalMasses[i];
                                }
                            }
                        }
                        break;
                }
            }
            else if (!isBound && attachedColliderHandleIndex >= 0)
            {
                attachedColliderHandleIndex = -1;
                m_Actor.SetConstraintsDirty(Oni.ConstraintType.Pin);
            }
        }

        private void BreakDynamicAttachment(float substepTime)
        {
            if (enabled && m_AttachmentType == AttachmentType.Dynamic && m_Actor.isLoaded && isBound)
            {
                var solver = m_Actor.solver;

                var actorConstraints = m_Actor.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints<ObiPinConstraintsBatch>;
                var solverConstraints = solver.GetConstraintsByType(Oni.ConstraintType.Pin) as ObiConstraints<ObiPinConstraintsBatch>;

                bool dirty = false;
                if (actorConstraints != null && pinBatch != null && actorConstraints.batchCount <= solverConstraints.batchCount)
                {
                    int pinBatchIndex = actorConstraints.batches.IndexOf(pinBatch);
                    if (pinBatchIndex >= 0 && pinBatchIndex < actor.solverBatchOffsets[(int)Oni.ConstraintType.Pin].Count)
                    {
                        int offset = actor.solverBatchOffsets[(int)Oni.ConstraintType.Pin][pinBatchIndex];
                        var solverBatch = solverConstraints.batches[pinBatchIndex];

                        float sqrTime = substepTime * substepTime;
                        for (int i = 0; i < pinBatch.activeConstraintCount; i++)
                        {
                            // In case the handle has been created/destroyed.
                            if (pinBatch.pinBodies[i] != attachedCollider.Handle)
                            {
                                pinBatch.pinBodies[i] = attachedCollider.Handle;
                                dirty = true;
                            }

                            // in case the constraint has been broken:
                            if (-solverBatch.lambdas[(offset + i) * 4 + 3] / sqrTime > breakThreshold)
                            {
                                pinBatch.DeactivateConstraint(i);
                                dirty = true;
                            }
                        }
                    }
                }

                // constraints are recreated at the start of a step.
                if (dirty)
                    m_Actor.SetConstraintsDirty(Oni.ConstraintType.Pin);
            }
        }
    }
}
